package edu.mit.csail.relo.jdt.builder.asm;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.openrdf.model.Resource;
import org.openrdf.model.Value;
import org.openrdf.rio.Parser;
import org.openrdf.rio.StatementHandler;
import org.openrdf.rio.StatementHandlerException;
import org.openrdf.sesame.sail.StatementIterator;

import edu.mit.csail.relo.ReloCore;
import edu.mit.csail.relo.jdt.RJCore;
import edu.mit.csail.relo.jdt.ReloJDTPlugin;
import edu.mit.csail.relo.jdt.builder.ReloASTExtractor;
import edu.mit.csail.relo.jdt.builder.ReloBuildVisitor;
import edu.mit.csail.relo.jdt.builder.ReloRDFBuildProcessor;
import edu.mit.csail.relo.store.ReloRdfRepository;
import edu.mit.csail.relo.store.StoreUtil;
import edu.mit.csail.relo.utils.LoadUtils;

/**
 * @author Cyrus J. Kalbrener
 */
public class AsmBuildVisitor extends ReloBuildVisitor {
	public static final String JARS_TO_BUILD = "jarsToBuild";
	public static final String JARS_TO_REMOVE = "jarsToRemove";
	static final Logger logger = ReloJDTPlugin.getLogger(AsmBuildVisitor.class);

	private String projectName;

	public AsmBuildVisitor(ReloRdfRepository reloRdf, List<ReloRDFBuildProcessor> buildProcessors, IProject project) {
        super(reloRdf, buildProcessors, project);
        projectName = project.getName();
        addPrimitives();
	}


	@Override
    public boolean visit(IResourceDelta delta) throws CoreException {
        boolean remAnn = false;
		boolean addAnn = false;
		switch (delta.getKind()) {
		case IResourceDelta.REMOVED:
			remAnn = true;
			break;
		case IResourceDelta.ADDED:
			addAnn = true;
			break;
		case IResourceDelta.CHANGED:
			remAnn = true;
			addAnn = true;
			break;
		}
		return visit(delta.getResource(), addAnn, remAnn);
	}
	


	@Override
	 public boolean visit(IResource resource) throws CoreException {
		try {
			// wrap around our logging framework to ease debugging, etc.
			return visitInternal(resource, true, false);
		} catch (Throwable t) {
			logger.error("Unexpected Exception", t);
		}
		return false;
	}
	
    public boolean visit(IResource resource, boolean addAnn, boolean remAnn) throws CoreException {
    	try {
			// wrap around our logging framework to ease debugging, etc.
			return visitInternal(resource, addAnn, remAnn);
		} catch (Throwable t) {
			logger.error("Unexpected Exception", t);
		}
		return false;
	}
	
	private static IProject cachedProject = null;
	private static IPath cachedProjOutputLoc = null;
	
    public boolean visitInternal(IResource resource, boolean addAnn, boolean remAnn) throws CoreException {
    	if(monitor.isCanceled())
    		return false;
		if (resource.getType() == IResource.PROJECT) {
			processJars((IProject) resource);
			return true;
		}
		if (resource.getType() != IResource.FILE) {
			return true;
		}
				
		IFile file = (IFile) resource;
		
		if(resource.getName().endsWith(".relo")) {
			parseReloFile(file, addAnn, remAnn);
			return true;
		}

		// process .class files
		if (file.getName().endsWith(".class")) {
			if (file.getProject() != cachedProject) {
				cachedProject = file.getProject();
				IJavaProject currJP = JavaCore.create(cachedProject);
				// we do a removeFirstSegment since the project name is repeated
				// we add the project location since the given information is only relative
				cachedProjOutputLoc = currJP.getProject().getLocation().append(currJP.getOutputLocation().removeFirstSegments(1));
			}
			
			processJavaFiles(resource, file, cachedProjOutputLoc, addAnn, remAnn);
		}
		
		return false;
	}

	private void processJavaFiles(IResource resource, IFile classFile, IPath currProjOutputLoc, boolean addAnn, boolean remAnn) throws JavaModelException {
		reloRdf.startTransaction();

		if(remAnn && !addAnn) {
			// file has been removed ==> remove statements
			Resource currFileRes = ReloCore.eclipseResourceToRDFResource(reloRdf, resource);
			List<Resource> srcResources = ReloASTExtractor.getResourceForFile(reloRdf, currFileRes);
			if (srcResources != null) {
				for(Resource res : srcResources) {				
					List<Resource> reloFiles = LoadUtils.getDependentReloFiles(res, reloRdf);
					reloRdf.removeStatements(res, null, null);
					LoadUtils.checkDependentReloFiles(resource, reloFiles, reloRdf);
					LoadUtils.checkBuildersListeners();
				}
			}
			return;
		}

		IPath fileLoc = classFile.getLocation();
		// get fully qualified name based on output locaiton
		fileLoc = fileLoc.removeFirstSegments(currProjOutputLoc.matchingFirstSegments(fileLoc)).setDevice(null);
		String name = fileLoc.toString().replace(".class", "").replaceAll("/", ".");
		
		List<Resource> reloFiles = null;
		Resource classKey = AsmUtil.getClassKey(name.replace(".class", ""), reloRdf);
		if(classKey!=null) {
			reloFiles = LoadUtils.getDependentReloFiles(classKey, reloRdf);
		}
		
		parseClassFile(name, addAnn, remAnn, classFile);
		reloRdf.addStatement(ReloCore.eclipseResourceToRDFResource(reloRdf, classFile), 
								ReloCore.name, 
								classFile.getName());
		
		if(reloFiles!=null) {
			LoadUtils.checkDependentReloFiles(resource, reloFiles, reloRdf);
			LoadUtils.checkBuildersListeners();
		}

		reloRdf.commitTransaction();

	}


	private void processJars(IProject projectRes) throws CoreException {
		Object buildProperty = projectRes.getSessionProperty(new QualifiedName(JARS_TO_BUILD, JARS_TO_BUILD));
		if (buildProperty != null && (buildProperty instanceof ArrayList)) {
			processJarsList((ArrayList) buildProperty, true, false);
		}
		Object removeProperty = projectRes.getSessionProperty(new QualifiedName(JARS_TO_REMOVE, JARS_TO_REMOVE));
		if (removeProperty != null && (removeProperty instanceof ArrayList)) {
			processJarsList((ArrayList)removeProperty, false, true);
		}
	}


	private void processJarsList(List jarsList, boolean add, boolean remove) {
		for(int i=0; i<jarsList.size(); i++) {
			try {
				String jarLocation = jarsList.get(i).toString();
				URL jarURL = new URL(jarLocation.replaceAll(" ", "%20").replaceAll("file://", "file:/"));
				URI uri = jarURL.toURI();
				parseArchive(new ZipFile(new File(uri)), add, remove);
			} catch(Exception e) {
				logger.error("Could not parse file. ", e);
			}
		}
	}
    
    private void parseArchive(ZipFile archive, boolean add, boolean remove) {
    	reloRdf.startTransaction();
    	int commitInterval = 1000;
    	int count = 0;
    	for (Enumeration en = archive.entries(); en.hasMoreElements();) {
    		if (++count % commitInterval == 0) {
    			reloRdf.commitTransaction();
    			reloRdf.startTransaction();
    		}
    		if (monitor.isCanceled()) {
    			reloRdf.commitTransaction();
    			return;
    		}
    		ZipEntry ze = (ZipEntry) en.nextElement();
    		
    		monitor.subTask(new StringBuffer("[")
    				.append(taskDone++)
    				.append("/")
    				.append(taskSize)
    				.append("] Parsing ")
    				.append(archive.getName())
    				.append(" for Relo ")
    				.append(ze.getName()).toString());
    		if (ze.getName().endsWith(".class")) {
    			try {
    				if(add && !remove) {
    					ClassReader cr = new ClassReader(archive.getInputStream(ze));
    					AsmClassVisitor classVisitor = new AsmClassVisitor(reloRdf, projectResource, projectName, null);
    					cr.accept(classVisitor, false);
    					runProcessors(classVisitor.getClassRes()); 
    				} else if (remove && !add) {
    					String name = ze.getName().replace(".class", "");
    					Resource classKey = AsmUtil.getClassKey(name, reloRdf);
    					if(classKey!=null) {
    						StatementIterator iter = reloRdf.getStatements(classKey, ReloCore.contains, null);
    						while(iter.hasNext()) {
    							Resource containedRes = (Resource) iter.next().getObject();
    							reloRdf.removeStatements(containedRes, null, null);
    							runProcessors(containedRes);
    						}					
    						reloRdf.removeStatements(classKey, null, null);
    						runProcessors(classKey);
    					}
    				}
				} catch (IOException e) {
					logger.error("Unexpected Exception", e);
				}
			} else if (ze.getName().endsWith(".jar") || ze.getName().endsWith(".zip")) {
				try {	
					String path = archive.getName().substring(0, archive.getName().lastIndexOf("\\") + 1) + ze.getName().replace("/", "-").replace("\\", "-");
					BufferedInputStream input = new BufferedInputStream(archive.getInputStream(ze));
					BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(path));
					
					int buffer_size = 1024 * 50;
					byte[] buffer = new byte[buffer_size];
					
					int len = 0;
					while ((len = input.read(buffer)) > 0) {
						output.write(buffer, 0, len);
					}
					input.close();
					output.flush();
					output.close();
					
					if(!(path.startsWith("file:"))) {
						path = "file:/" + path;
					}
					URL noAuthorityURL = new URL(path.replaceAll(" ", "%20").replaceAll("file://", "file:/"));
					URI entryURI = noAuthorityURL.toURI();
					File file = new File(entryURI);
					ZipFile zip = new ZipFile(file);
				 	parseArchive(zip, add, remove);
					file.delete();

				} catch(Exception e) {
					logger.error("Could not parse file " + ze.getName(), e);
				}
			}
			monitor.worked(1);
		}
		reloRdf.commitTransaction();
	}

	/**
	 * @param className
	 * @param add
	 * @param remove
	 * @param icu
	 */
	private void parseClassFile(String className, boolean add, boolean remove, IFile classFile) {
		if (monitor.isCanceled()) {
			return;
		}
		className = AsmUtil.quickReplace(className, ".", "/").append(".class").toString();
		monitor.subTask(new StringBuffer("[")
								.append(taskDone++)
								.append("/")
								.append(taskSize)
								.append("] Parsing ")
								.append(className)
								.append(" for Relo ").toString());

		try {
			InputStream classStream = classFile.getContents();
			if(remove) {
				Resource classKey = AsmUtil.getClassKey(className.replace(".class", ""), reloRdf);
				if(classKey!=null) {
					StatementIterator iter = reloRdf.getStatements(classKey, ReloCore.contains, null);
					while(iter.hasNext()) {
						Resource containedRes = (Resource) iter.next().getObject();
						reloRdf.removeStatements(containedRes, null, null);
						runProcessors(containedRes);
					}
					reloRdf.removeStatements(classKey, null, null);
					runProcessors(classKey);
				}
			}
			if(add) {
				ClassReader cr = new ClassReader(classStream);
				AsmClassVisitor classVisitor = new AsmClassVisitor(reloRdf, projectResource, projectName, classFile);
				cr.accept(new ClassAdapter(classVisitor), false);
				runProcessors(classVisitor.getClassRes());  
			}
		} catch (Throwable e) {
			logger.error("Unexpected Exception, trying to parse: " + className + " loc: " + classFile.getFullPath(), e);
		}
		monitor.worked(1);
	}
	
	/**
	 * If .relo file is deleted, removes all statements that were put 
	 * in the repository when the .relo file was created. If .relo file 
	 * is added, add all its statements back into the repository
	 *
	 */
	private void parseReloFile(IFile reloFile, boolean add, boolean remove) {
		final Resource reloRes = ReloCore.eclipseResourceToRDFResource(reloRdf, reloFile);
		if(remove) {
			//Remove all statements that were added when the .relo file was created
			StatementIterator containsIter = reloRdf.getStatements(reloRes, ReloCore.contains, null);
			reloRdf.startTransaction();
			while(containsIter.hasNext()) {
				Resource res = (Resource) containsIter.next().getObject();
				reloRdf.removeStatements(res, null, null);
			}
			reloRdf.commitTransaction();
			
			reloRdf.startTransaction();
			reloRdf.removeStatements(reloRes, null, null);
			reloRdf.commitTransaction();
		} 
		
		if (add) {
			//Read and parse the contents of the .relo file and add the statements to the repository
			try {
				reloRdf.startTransaction();
				reloRdf.addStatement(reloRes, reloRdf.rdfType, ReloCore.reloFile);
				reloRdf.commitTransaction();
				
				BufferedReader reader = new BufferedReader(new InputStreamReader(reloFile.getContents(true)));
				Parser rdfParser = StoreUtil.getRDFParser(reloRdf);
				rdfParser.setStatementHandler(new StatementHandler() {
					public void handleStatement(Resource subj, org.openrdf.model.URI pred, Value obj) throws StatementHandlerException {
						reloRdf.addStatement(reloRes, ReloCore.contains, subj);
						reloRdf.addStatement(subj, pred, obj);
					}});
				
				reloRdf.startTransaction();
				rdfParser.parse(reader, ReloRdfRepository.reloRdfNamespace);
				reloRdf.commitTransaction();
				reader.close();
			} catch (Exception e) {
				logger.error("Unexpected Error", e);
			}
			List<Resource> reloFiles = new ArrayList<Resource>();
			reloFiles.add(reloRes);
			LoadUtils.checkDependentReloFiles(reloFile, reloFiles, reloRdf);
		}
	}


	private void addPrimitives() {
		ReloRdfRepository reloRdf = StoreUtil.getDefaultStoreRepository();
		String[] types = new String[] { 
				"B", "C", "D", "F", "I", "J", "V", "Z",
				"S", "LList;", "LMap;", "LSet;", "LString;" };
		String[] subTypes = new String[] { "", "[]", "[][]" };
		reloRdf.startTransaction();

		for (int i = 0; i < types.length; i++) {
			String name = Type.getType(types[i]).getClassName();

			for (int j = 0; j < subTypes.length; j++) {
				// vineet: bug? this seems to be incorrect
				Resource uri = reloRdf.getDefaultURI("jdt-wkspc#$" + name + subTypes[j]);

				if (reloRdf.contains(uri, ReloCore.name, (Value) null)) {
					continue;
				}

				reloRdf.addStatement(uri, ReloCore.name, name + subTypes[j]);
				reloRdf.addStatement(uri, ReloCore.initialized, true);
				reloRdf.addStatement(uri, reloRdf.rdfType, RJCore.classType);
			}
		}
		reloRdf.commitTransaction();
	}
}
